/*
 * Copyright (c) 2001 Sun Microsystems, Inc.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *       Sun Microsystems, Inc. for Project JXTA."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact Project JXTA at http://www.jxta.org.
 *
 * 5. Products derived from this software may not be called "JXTA",
 *    nor may "JXTA" appear in their name, without prior written
 *    permission of Sun.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of Project JXTA.  For more
 * information on Project JXTA, please see
 * <http://www.jxta.org/>.
 *
 * This license is based on the BSD license adopted by the Apache Foundation.
 *
 * $Id: CMS.java,v 1.30 2006/02/07 20:43:49 bondolo Exp $
 *
 */

package net.jxta.share;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;

import net.jxta.discovery.DiscoveryService;
import net.jxta.document.Advertisement;
import net.jxta.document.MimeMediaType;
import net.jxta.endpoint.EndpointService;
import net.jxta.endpoint.EndpointAddress;
import net.jxta.endpoint.EndpointListener;
import net.jxta.endpoint.Message;
import net.jxta.endpoint.MessageElement;
import net.jxta.endpoint.ByteArrayMessageElement;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.ID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.platform.Application;
import net.jxta.protocol.ResolverQueryMsg;
import net.jxta.protocol.ResolverResponseMsg;
import net.jxta.resolver.ResolverService;
import net.jxta.resolver.QueryHandler;


import org.apache.log4j.Logger;
import org.apache.log4j.Level;

/**
 * This class implements the Content Management Service.
 * The class is started with one of the startApp methods.
 * The command pipe and persistent directory is passed to the startApp method, else
 * default values are used.
 * A remote client interacts with the service by finding the request PipeAdvertisement
 * and sending it LIST and GET requests. The requests are handled by the 
 * ListMessageProcessor and GetMessageProcessor.
 */

public class CMS implements Application, EndpointListener, QueryHandler {

    private final static transient Logger LOG = Logger.getLogger(CMS.class.getName());

    public static final String DEFAULT_DIR = "cms";

    // Message tag's
    public static final String LIST_REQUEST = "LIST_REQ";
    public static final String LIST_RESULT = "LIST_RES";
    public static final String GET_REQUEST = "GET_REQ";
    public static final String GET_RESULT = "GET_RES";

    public static final String MESSAGE_TYPE = "msgType";
    public static final String REQUEST_ID = "RID";
    public static final String CONTENT_ID = "CID";
    public static final String RETURN_ADDRESS = "RTN_ADR";
    public static final String CONTENT_ADVERTISEMENT = "CADV";
    public static final String CONTENT_LENGTH = "CLEN";
    public static final String ADVERTISEMENT_COUNT = "COUNT";
    public static final String CHUNK_OFFSET = "OFFSET";
    public static final String CHUNK_SIZE = "SIZE";
    public static final String CHUNK_DATA = "DATA";
    public static final String RANGE_BEGIN = "RANGE_BEGIN";
    public static final String RANGE_END = "RANGE_END";

    public static final String QUERY_SUBSTRING = "QSUBSTR";

    public static String serviceName = "CMS";
    public static MimeMediaType encodeAs = new MimeMediaType("text", "xml");


    // The following two constants defines how long ContentAdvertisements are published
    // in Discovery.
    // FIXME: 11/03/2002 lomax@jxta.org We need to have a better lifecycle management
    // of the advertisements. Current settings are probably fine for usual usage of CMS
    // for the time being.

    private static long DEFAULT_LOCAL_ADV_LIFETIME = DiscoveryService.DEFAULT_LIFETIME;
    private static long DEFAULT_REMOTE_ADV_LIFETIME = 2 * 60 * 1000L; // 2 minutes

    private PeerGroup group = null;
    private ListMessageProcessor listQueue = null;
    private GetMessageProcessor getQueue = null;
    private ContentManager contentManager = null;
    private File cmsDir = null;


    public CMS() {
        // Set the default content manager directory to a subdirectory of the
        // JXTA_HOME directory.
        String homedir = System.getProperty("JXTA_HOME");
        homedir = (homedir != null) ? homedir + DEFAULT_DIR : DEFAULT_DIR;
        cmsDir = new File(homedir);

        if (LOG.isEnabledFor(Level.INFO))
            LOG.info("create new CMS");
        // Makes sure the AdvertisementFactory knows about ContentAdvertisement
        ContentAdvertisementImpl.registerAdvertisement();
    }

    /**
     * Initialize the application passing it ist peer group handle.
     *
     * @param group PeerGroup this application is started from
     *
     * @exception PeerGroupException failure to initialize the peer group application
     *
     */

    public void init(PeerGroup group,
                     ID assignedID,
                     Advertisement impl)
    throws PeerGroupException {

        this.group = group;
    }
    /**
     * Start the service with the given parameters.
     * The parameters are in the form of name=value pairs.
     * There is one optional parameter, the directory for storing
     * persistent data about the shared content.  
     *
     * @param arg String[] of parameters
     *
     * @return int status indication. 0 if ok, -1 if error
     */
    public int startApp(String[] arg) {
        File cmsDir = null;

        for (int i=0; i<arg.length; i++) {
            // Directory parameter
            if (arg[i].startsWith("dir=")) {
                cmsDir = new File(arg[i].substring(4));
            }
        }

        return startApp(cmsDir);
    }

    /**
     * Start the service with the given parameters.
     *
     * @param rPipeAdv PipeAdvertisement of the request pipe
     * @param cmsDir File pointing to the persistence directory
     *
     * @return int status indication. 0 if ok, -1 if error.
     */
    public int startApp(File dir) {
        // check if a cmsDir was passed
        if(dir != null)
            cmsDir = dir;

        // Check if the cms directory exists
        if (false == cmsDir.exists()) {
            // create the directory
            cmsDir.mkdir();
        }

        // Initialize the content manager
        try {
            contentManager = new ContentManagerImpl(cmsDir, this);
        } catch (IOException e) {
            if (LOG.isEnabledFor(Level.ERROR))
                LOG.error("Could not initialize content manager");
            return (-1);
        }

        // initialize the message processor queues
        listQueue = new ListMessageProcessor(this);
        getQueue = new GetMessageProcessor(this);

        // register the message listener
        group.getEndpointService().addIncomingMessageListener(this,serviceName, getGroupId(group));
        // Publish the contentManager
        publishAllContents ();

        return 0;
    }

    /**
     * Stops the service
     */
    public void stopApp() {
        // unregister the message listener
        group.getEndpointService().removeIncomingMessageListener(serviceName, getGroupId(group));

        group.getResolverService().unregisterHandler(serviceName + getGroupId (group));
    }

    /**
     * Add a SearchListener object to monitor the search queries recieved.
     *
     * @param sl the SearchListener to add
     * @see SearchListener
     * @see #removeSearchListener(SearchListener)
     */
    public void addSearchListener(SearchListener sl) {
        if(listQueue != null)
            listQueue.addSearchListener(sl);
    }

    /**
     * @param sl the SearchListener object to remove.
     * @see SearchListener
     * @see #addSearchListener(SearchListener)
     */
    public void removeSearchListener(SearchListener sl) {
        if(listQueue != null)
            listQueue.removeSearchListener(sl);
    }

    /**
     * Returns the EndpointService for this service.
     *
     * @return EndpointService EndpointService for this service.
     *
     */
    public EndpointService getEndpointService() {
        return group.getEndpointService();
    }

    /**
     * Returns the Content Manager for this service.
     *
     * @return ContentManager the Content Manager.
     *
     */
    public ContentManager getContentManager() {
        return contentManager;
    }

    /**
     * Returns the endpoint address for this service.
     *
     * @return String the endpoint address.
     *
     */
    public String getEndpointAddress() {
        return "jxta://" + getPeerId(group) +
               "/" + serviceName +
               "/" + getGroupId(group);
    }

    public void processIncomingMessage(Message message,
                                       EndpointAddress srcAddr, EndpointAddress dstAddr) {

        if (LOG.isEnabledFor(Level.DEBUG)) {
            LOG.debug("processing an Incoming Message");
        }

        if (message != null) {
            // Handle the message
            try {
                String messageType = popString(message, MESSAGE_TYPE);

                if (LOG.isEnabledFor(Level.DEBUG)) {
                    LOG.debug("handleMessage type = " + messageType);
                }
                if (LIST_REQUEST.equals(messageType)) {
                    // queue up on the list command queue
                    listQueue.push(message);
                } else
                    if (GET_REQUEST.equals(messageType)) {
                        // queue up on the get command queue
                        getQueue.push(message);
                    }
            } catch (IOException e) {}
        }
    }

    /**
     * Process the resolver query, and generate response
     * @return GenericResolverMsg Response to the query
     * @param query ResolverQueryMsg query
     *
     * @exception NoResponseException is thrown when the resolver service
     * does not have an response and is not interested by the response.
     * @exception ResendQueryException is thrown when the resolver service
     * does not have a response, but is interested by the response. In that
     * case, the resolver service is responsible for forward the response
     * to the original peer that has issued the query.
     * @exception DiscardQueryException is thrown when the resolver service
     * has decided that the query should simply be ignored.
     * @exception IOException is thrown when the service resolver was not able to
     * process the query
     *
     */

    public int processQuery(ResolverQueryMsg query) {

        String queryStr = query.getQuery();

        if (LOG.isEnabledFor(Level.DEBUG))
            LOG.debug("received query: "+queryStr);

        int index = queryStr.indexOf(';');
        String addressStr = queryStr.substring(0, index);
        String subString = queryStr.substring(index+1+"substring=".length());

        if (LOG.isEnabledFor(Level.DEBUG)) {
            LOG.debug("addressStr = "+addressStr);
            LOG.debug("substring = "+subString);
        }

        //should it have a namespace?
        Message message = new Message();

        message.addMessageElement(
            new ByteArrayMessageElement(CMS.REQUEST_ID,encodeAs
                                        ,new String("none").getBytes(),null));

        message.addMessageElement(
            new ByteArrayMessageElement(CMS.RETURN_ADDRESS, encodeAs
                                        ,addressStr.getBytes(),null));

        if (null != subString) {
            message.addMessageElement(
                new ByteArrayMessageElement(CMS.QUERY_SUBSTRING, encodeAs
                                            ,subString.getBytes(),null));
        }

        // queue up on the list command queue
        listQueue.push(message);

        return ResolverService.Repropagate;
    }

    /**
     * call back method, when messages are received by the ResolverService
     * it calls back this method to deal with received responses
     * @param response ResolverQueryMsg reponse
     *
     */

    public void processResponse(ResolverResponseMsg response) {
        if (LOG.isEnabledFor(Level.DEBUG))
            LOG.debug("received response: "+response.getResponse());

    }

    /**
     * Returns the String contained in the given tag in the given Message.
     *
     * @return String from given tag in given message.
     *
     */
    public static String popString(Message message, String tag) throws IOException {
        MessageElement substrQueryEl = message.getMessageElement(tag);
        if(substrQueryEl == null) {
            if(LOG.isInfoEnabled())
                LOG.info("message is missing a \"" + tag +
                         "\" element.");
            return "";
        }
        return substrQueryEl.toString();
    }

    /**
     * Returns the String version of the groupId for the given group.
     *
     * @return String groupId for the given group
     *
     */
    public static String getGroupId(PeerGroup group) {
        // XXX  20011105    bondolo@jxta.org why not just use the urn value?
        return group.getPeerGroupID().getUniqueValue().toString();
    }

    /**
     * Returns the String version of the groupId for the given group.
     *
     * @return String peerId for the given group
     *
     */
    public static String getPeerId(PeerGroup group) {
        // XXX  20011105    bondolo@jxta.org why not just use the urn value?
        return group.getPeerID().getUniqueValue().toString();
    }

    /**
     * Returns a ContentAdvertisement create from the given stream
     *
     * @return ContentAdvertisement the created ContentAdvertisement
     *
     */
    public static ContentAdvertisement createContentAdvertisement(InputStream is)
    throws IOException {
        return new ContentAdvertisementImpl(is);
    }

    /**
     * Publish into Discovery the ContentAdvertisement of the ContentManager.
     **/
    private void publishAllContents () {

        DiscoveryService disco = (DiscoveryService) group.getDiscoveryService();
        Content[] contents = null;

        if (contentManager == null) {
            // Nothing to share.
            return;
        }

        contents = contentManager.getContent();
        if ((contents == null) || (contents.length == 0)) {
            // Nothing to share.
            return;
        }

        for (int i = 0; i < contents.length; ++i) {
            try {
                Content content = contents [i];
                publishContent (content);
            } catch (Exception ez) {
                System.err.println ("Can't publish advertisement: " + ez);
                continue;
            }
        }
    }

    /**
     * Publish into Discovery the ContentAdvertisement of the ContentManager.
     **/
    protected void publishContent (Content content) {

        if (content == null) {
            // No content. Nothing to do
            return;
        }

        DiscoveryService disco = (DiscoveryService) group.getDiscoveryService();

        try {
            ContentAdvertisement adv = content.getContentAdvertisement();
            adv.setAddress (getEndpointAddress());
            disco.publish (adv,
                           DEFAULT_LOCAL_ADV_LIFETIME,
                           DEFAULT_REMOTE_ADV_LIFETIME);

        } catch (Exception ez) {
            System.err.println ("Can't publish advertisement: " + ez);
            return;
        }
    }

    protected void forgetContent (Content content) {
        // FIXME 11/03/2002 lomax@jxta.org TBD
    }

    public void finalize () {
        stopApp();
    }
}
